/*
 * Copyright (c) 2012 Weongyo Jeong <weongyo@gmail.com>
 * Copyright (c) 1983, 1991, 1993, 1994
 *	The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * This is a simple watchdog daemon lauching our server applications
 * and monitoring its alive.  When one is crashed by some reason, it'll
 * recover it automatically.
 *
 * This is developed for Mudfish (http://en.loxch.com).
 */

#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <grp.h>
#ifdef __FreeBSD__
#include <libutil.h>
#endif
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <unistd.h>

#include "vpf.c"
#include "vqueue.h"

#define	SIGBLOCK	(sigmask(SIGCHLD)|sigmask(SIGHUP))

struct servtab {
	int		se_fd[2];	/* open descriptor */
	char		*se_service;	/* name of service */
	char		*se_server;	/* server program */
	u_char		se_checked;	/* looked at during merge */
	pid_t		se_pid;
	char		*se_user;	/* user name to run as */
	char		*se_group;	/* group name to run as */
	int		se_count;	/* number started since se_time */
	struct	timeval se_time;	/* start of se_count */
#define	MAXARGV 200
	char		*se_argv[MAXARGV+1];	/* program arguments */
	struct se_flags {
		u_int	se_reset : 1;
	} se_flags;
#define	se_reset		se_flags.se_reset
	struct	servtab	*se_next;
};

static struct servtab	*servtab;
static struct servtab	serv;
static FILE		*fconfig;
static char		line[LINE_MAX];
static const char	*CONFIG = "/etc/muddog.conf";
static fd_set		allsock;
static int		debug;
static int		maxsock;	/* highest-numbered descriptor */
static int		signalpipe[2];
static struct sigaction sachld, sahup, sapipe;

static void	enable(struct servtab *sep);
static void	disable(struct servtab *sep);

static void
usage(void)
{

	fprintf(stderr, "usage: muddog [-dv] [-P pidfile]\n");
	exit(EX_USAGE);
}

static void
version(void)
{

	fprintf(stderr, "muddog v" PACKAGE_VERSION "\n");
	exit(0);
}

/*
 * Add a signal flag to the signal flag queue for later handling
 */

static void
flag_signal(int c)
{
	char ch = c;

	if (write(signalpipe[1], &ch, 1) != 1) {
		syslog(LOG_ERR, "write: %m");
		_exit(EX_OSERR);
	}
}

static void
flag_config(int signo)
{

	(void)signo;
	flag_signal('H');
}

/*
 * Some child process has exited. See if it's on somebody's list.
 */
static void
flag_reapchild(int signo)
{

	(void)signo;
	flag_signal('C');
}

static void
reapchild(void)
{
	int status;
	pid_t pid;
	struct servtab *sep;

	for (;;) {
		pid = wait3(&status, WNOHANG, (struct rusage *)0);
		if (pid <= 0)
			break;
		if (debug)
			warnx("%d reaped, %s %u", pid,
			    WIFEXITED(status) ? "status" : "signal",
			    WIFEXITED(status) ? WEXITSTATUS(status)
				: WTERMSIG(status));
		for (sep = servtab; sep; sep = sep->se_next) {
			if (sep->se_pid != pid)
				continue;
			sep->se_pid = -1;
			enable(sep);
			if (WIFSIGNALED(status) || WEXITSTATUS(status))
				syslog(LOG_WARNING,
				    "%s[%d]: exited, %s %u",
				    sep->se_server, pid,
				    WIFEXITED(status) ? "status" : "signal",
				    WIFEXITED(status) ? WEXITSTATUS(status)
					: WTERMSIG(status));
			break;
		}
	}
}

static int
setconfig(void)
{

	if (fconfig != NULL) {
		fseek(fconfig, 0L, SEEK_SET);
		return (1);
	}
	fconfig = fopen(CONFIG, "r");
	return (fconfig != NULL);
}

static void
endconfig(void)
{
	if (fconfig) {
		(void) fclose(fconfig);
		fconfig = NULL;
	}
}

static void
freeconfig(struct servtab *cp)
{
	int i;

	if (cp->se_service)
		free(cp->se_service);
	if (cp->se_user)
		free(cp->se_user);
	if (cp->se_group)
		free(cp->se_group);
	if (cp->se_server)
		free(cp->se_server);
	for (i = 0; i < MAXARGV; i++)
		if (cp->se_argv[i])
			free(cp->se_argv[i]);
}

/*
 * Finish with a service and its socket.
 */
static void
close_sep(struct servtab *sep)
{

	if (sep->se_fd[0] >= 0 && sep->se_fd[1] >= 0) {
		if (FD_ISSET(sep->se_fd[0], &allsock))
			disable(sep);
		(void) close(sep->se_fd[0]);
		(void) close(sep->se_fd[1]);
		sep->se_fd[0] = -1;
		sep->se_fd[1] = -1;
	}
	sep->se_count = 0;
}

static char *
nextline(FILE *fd)
{
	char *cp;

	if (fgets(line, sizeof (line), fd) == NULL)
		return ((char *)0);
	cp = strchr(line, '\n');
	if (cp)
		*cp = '\0';
	return (line);
}

static char *
skip(char **cpp)
{
	char *cp = *cpp;
	char *start;
	char quote = '\0';

again:
	while (*cp == ' ' || *cp == '\t')
		cp++;
	if (*cp == '\0') {
		int c;

		c = getc(fconfig);
		(void) ungetc(c, fconfig);
		if (c == ' ' || c == '\t')
			if ((cp = nextline(fconfig)))
				goto again;
		*cpp = (char *)0;
		return ((char *)0);
	}
	if (*cp == '"' || *cp == '\'')
		quote = *cp++;
	start = cp;
	if (quote)
		while (*cp && *cp != quote)
			cp++;
	else
		while (*cp && *cp != ' ' && *cp != '\t')
			cp++;
	if (*cp != '\0')
		*cp++ = '\0';
	*cpp = cp;
	return (start);
}

/*
 * Safe skip - if skip returns null, log a syntax error in the
 * configuration file and exit.
 */
static char *
sskip(char **cpp)
{
	char *cp;

	cp = skip(cpp);
	if (cp == NULL) {
		syslog(LOG_ERR, "%s: syntax error", CONFIG);
		exit(EX_DATAERR);
	}
	return (cp);
}

static char *
newstr(const char *cp)
{
	char *cr;

	if ((cr = strdup(cp != NULL ? cp : "")))
		return (cr);
	syslog(LOG_ERR, "strdup: %m");
	exit(EX_OSERR);
}

static struct servtab *
getconfigent(void)
{
	struct servtab *sep = &serv;
	int argc;
	char *arg, *cp, *s;

more:
	while ((cp = nextline(fconfig)) != NULL) {
		if (*cp == '#' || *cp == '\0')
			continue;
		break;
	}
	if (cp == NULL)
		return ((struct servtab *)0);
	/*
	 * clear the static buffer, since some fields (se_ctrladdr,
	 * for example) don't get initialized here.
	 */
	memset(sep, 0, sizeof *sep);
	arg = skip(&cp);
	if (cp == NULL) {
		/* got an empty line containing just blanks/tabs. */
		goto more;
	}
	sep->se_service = newstr(arg);
	sep->se_user = newstr(sskip(&cp));
	if ((s = strrchr(sep->se_user, ':')) != NULL) {
		*s = '\0';
		sep->se_group = newstr(s + 1);
	} else
		sep->se_group = NULL;
	sep->se_server = newstr(sskip(&cp));
	argc = 0;
	for (arg = skip(&cp); cp; arg = skip(&cp))
		if (argc < MAXARGV) {
			sep->se_argv[argc++] = newstr(arg);
		} else {
			syslog(LOG_ERR,
				"%s: too many arguments for service %s",
				CONFIG, sep->se_service);
			goto more;
		}
	while (argc <= MAXARGV)
		sep->se_argv[argc++] = NULL;
	return (sep);
}

static sigset_t
mud_sigblock(void)
{
	sigset_t mask, omask;

	sigemptyset(&mask);
	sigaddset(&mask, SIGCHLD);
	sigaddset(&mask, SIGHUP);
	sigprocmask(SIG_BLOCK, &mask, &omask);
	return (omask);
}

static void
mud_sigsetmask(sigset_t mask)
{

	sigprocmask(SIG_SETMASK, &mask, NULL);
}

static void
mud_sigsetmask_zero(void)
{
	sigset_t mask;

	sigemptyset(&mask);
	sigprocmask(SIG_SETMASK, &mask, NULL);
}

static struct servtab *
enter(struct servtab *cp)
{
	struct servtab *sep;
	sigset_t omask;

	sep = (struct servtab *)malloc(sizeof (*sep));
	if (sep == (struct servtab *)0) {
		syslog(LOG_ERR, "malloc: %m");
		exit(EX_OSERR);
	}
	*sep = *cp;
	sep->se_fd[0] = -1;
	sep->se_fd[1] = -1;
	omask = mud_sigblock();
	sep->se_next = servtab;
	servtab = sep;
	mud_sigsetmask(omask);
	return (sep);
}

static void
enable(struct servtab *sep)
{
	ssize_t l;

	if (debug)
		warnx("enabling %s, fd[0] %d fd[1] %d", sep->se_service,
		    sep->se_fd[0], sep->se_fd[1]);
	l = write(sep->se_fd[1], "L", sizeof("L") - 1);
	assert(l == 1);

	FD_SET(sep->se_fd[0], &allsock);
	if (sep->se_fd[0] > maxsock)
		maxsock = sep->se_fd[0];
}

static void
disable(struct servtab *sep)
{
	if (debug)
		warnx(
		    "disabling %s, fd[0] %d fd[1] %d", sep->se_service,
		    sep->se_fd[0], sep->se_fd[1]);
	FD_CLR(sep->se_fd[0], &allsock);
	if (sep->se_fd[0] == maxsock)
		maxsock--;
}

static void
setup(struct servtab *sep)
{
	int r;

	r = pipe(sep->se_fd);
	assert(r == 0);
	enable(sep);
	if (debug)
		warnx("registered %s on fd[0] %d fd[1] %d",
		    sep->se_server, sep->se_fd[0], sep->se_fd[1]);
}

/*
 * print_service:
 *	Dump relevant information to stderr
 */
static void
print_service(const char *action, const struct servtab *sep)
{
	fprintf(stderr,
	    "%s: %s server=%s user=%s group=%s\n",
	    action, sep->se_service, sep->se_server, sep->se_user,
	    sep->se_group);
}

static void
config(void)
{
	struct servtab *sep, *new, **sepp;
	sigset_t omask;

	if (!setconfig()) {
		syslog(LOG_ERR, "%s: %m", CONFIG);
		return;
	}
	for (sep = servtab; sep; sep = sep->se_next)
		sep->se_checked = 0;
	while ((new = getconfigent())) {
		if (getpwnam(new->se_user) == NULL) {
			syslog(LOG_ERR,
			    "%s: no such user '%s', service ignored",
			    new->se_service, new->se_user);
			continue;
		}
		if (new->se_group && getgrnam(new->se_group) == NULL) {
			syslog(LOG_ERR,
			    "%s: no such group '%s', service ignored",
			    new->se_service, new->se_group);
			continue;
		}
		for (sep = servtab; sep; sep = sep->se_next)
			if (strcmp(sep->se_service, new->se_service) == 0)
				break;
		if (sep != NULL) {
			int i;

#define SWAP(t,a, b) { t c = a; a = b; b = c; }
			omask = mud_sigblock();
			/* might need to turn off service now */
			if (sep->se_fd[0] >= 0 && sep->se_fd[1] >= 0) {
				if (FD_ISSET(sep->se_fd[0], &allsock))
					disable(sep);
			}
			SWAP(char *, sep->se_user, new->se_user);
			SWAP(char *, sep->se_group, new->se_group);
			SWAP(char *, sep->se_server, new->se_server);
			for (i = 0; i < MAXARGV; i++)
				SWAP(char *, sep->se_argv[i], new->se_argv[i]);
			mud_sigsetmask(omask);
			freeconfig(new);
			if (debug)
				print_service("REDO", sep);
		} else {
			sep = enter(new);
			if (debug)
				print_service("ADD ", sep);
		}
		sep->se_checked = 1;
		sep->se_reset = 0;
		if (sep->se_fd[0] == -1 || sep->se_fd[1] == -1)
			setup(sep);
	}
	endconfig();
	/*
	 * Purge anything not looked at above.
	 */
	omask = mud_sigblock();
	sepp = &servtab;
	while ((sep = *sepp)) {
		if (sep->se_checked) {
			sepp = &sep->se_next;
			continue;
		}
		*sepp = sep->se_next;
		if (sep->se_fd[0] >= 0 || sep->se_fd[1] >= 0)
			close_sep(sep);
		if (debug)
			print_service("FREE", sep);
		freeconfig(sep);
		free(sep);
	}
	(void) mud_sigsetmask(omask);
}

/*
 * Record a new child pid for this service. If we've reached the
 * limit on children, then stop accepting incoming requests.
 */
static void
setchild(struct servtab *sep, pid_t pid)
{

	sep->se_pid = pid;
}

static int
launch(struct servtab *sep)
{
	struct passwd *pwd;
	struct group *grp;
	pid_t pid = 0;

	(void) mud_sigblock();
	if (sep->se_count++ == 0)
		(void)gettimeofday(&sep->se_time, (struct timezone *)NULL);
	pid = fork();
	if (pid < 0) {
		syslog(LOG_ERR, "fork: %m");
		mud_sigsetmask_zero();
		sleep(1);
		return (-1);
	}
	if (pid)
		setchild(sep, pid);
	mud_sigsetmask_zero();
	if (pid == 0) {
		sigaction(SIGCHLD, &sachld, (struct sigaction *)0);
		sigaction(SIGHUP, &sahup, (struct sigaction *)0);
		/* SIGPIPE reset before exec */
		if (debug)
			warnx("%d execl %s",
			    getpid(), sep->se_server);
		dup2(0, 1);
		dup2(0, 2);
		if ((pwd = getpwnam(sep->se_user)) == NULL) {
			syslog(LOG_ERR,
			    "%s: %s: no such user",
			    sep->se_service, sep->se_user);
			_exit(EX_NOUSER);
		}
		grp = NULL;
		if (sep->se_group != NULL &&
		    (grp = getgrnam(sep->se_group)) == NULL) {
			syslog(LOG_ERR,
			    "%s: %s: no such group",
			    sep->se_service, sep->se_group);
			_exit(EX_NOUSER);
		}
		if (grp != NULL)
			pwd->pw_gid = grp->gr_gid;
		if (setsid() < 0) {
			syslog(LOG_ERR, "%s: can't setsid(): %m",
			    sep->se_service);
			/* _exit(EX_OSERR); not fatal yet */
		}
		if (pwd->pw_uid) {
			if (setlogin(sep->se_user) < 0) {
				syslog(LOG_ERR,
				    "%s: can't setlogin(%s): %m",
				    sep->se_service, sep->se_user);
				/* _exit(EX_OSERR); not yet */
			}
			if (setgid(pwd->pw_gid) < 0) {
				syslog(LOG_ERR,
				    "%s: can't set gid %d: %m",
				    sep->se_service, pwd->pw_gid);
				_exit(EX_OSERR);
			}
			(void) initgroups(pwd->pw_name,
			    pwd->pw_gid);
			if (setuid(pwd->pw_uid) < 0) {
				syslog(LOG_ERR,
				    "%s: can't set uid %d: %m",
				    sep->se_service, pwd->pw_uid);
				_exit(EX_OSERR);
			}
		}
		sigaction(SIGPIPE, &sapipe, (struct sigaction *)0);
		execv(sep->se_server, sep->se_argv);
		syslog(LOG_ERR,
		    "cannot execute %s: %m", sep->se_server);
		_exit(0);
	}
	return (0);
}

static void
set_coredump(void)
{
    struct rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };

    setrlimit(RLIMIT_CORE, &core_limit);
}

int
main(int argc, char **argv)
{
	struct servtab *sep;
	struct sigaction sa;
	struct vpf_fh *pfh = NULL;
	const char *P_arg = "/var/run/muddog.pid";
	ssize_t l;
	int ch;

	set_coredump();
	openlog("muddog", LOG_PID | LOG_NOWAIT | LOG_PERROR, LOG_DAEMON);

	while ((ch = getopt(argc, argv, "dP:v")) != -1)
		switch (ch) {
		case 'd':
			debug = 1;
			break;
		case 'P':
			P_arg = optarg;
			break;
		case 'v':
			version();
			/* NOTREACHED */
		case '?':
		default:
			usage();
			/* NOTREACHED */
		}
	argc -= optind;
	argv += optind;
	if (argc > 0)
		CONFIG = argv[0];
	if (access(CONFIG, R_OK) < 0)
		syslog(LOG_ERR, "Accessing %s: %m, continuing anyway.", CONFIG);
	if (debug == 0) {
		if (P_arg && (pfh = VPF_Open(P_arg, 0644, NULL)) == NULL) {
			perror(P_arg);
			exit(1);
		}
		if (daemon(0, 0) < 0) {
			syslog(LOG_WARNING, "daemon(0,0) failed: %m");
		}
		/* From now on we don't want syslog messages going to stderr. */
		closelog();
		openlog("muddog", LOG_PID | LOG_NOWAIT, LOG_DAEMON);

		if (pfh != NULL && VPF_Write(pfh))
			syslog(LOG_ERR, "NOTE: Could not write PID file");
	}

	sa.sa_flags = 0;
	sigemptyset(&sa.sa_mask);
	sigaddset(&sa.sa_mask, SIGCHLD);
	sigaddset(&sa.sa_mask, SIGHUP);
	config();
	sa.sa_handler = flag_config;
	sigaction(SIGHUP, &sa, &sahup);
	sa.sa_handler = flag_reapchild;
	sigaction(SIGCHLD, &sa, &sachld);
	sa.sa_handler = SIG_IGN;
	sigaction(SIGPIPE, &sa, &sapipe);

	if (pipe(signalpipe) != 0) {
		syslog(LOG_ERR, "pipe: %m");
		exit(EX_OSERR);
	}
	if (fcntl(signalpipe[0], F_SETFD, FD_CLOEXEC) < 0 ||
	    fcntl(signalpipe[1], F_SETFD, FD_CLOEXEC) < 0) {
		syslog(LOG_ERR, "signalpipe: fcntl (F_SETFD, FD_CLOEXEC): %m");
		exit(EX_OSERR);
	}
	FD_SET(signalpipe[0], &allsock);

	for (;;) {
		int n;
		fd_set readable;

		readable = allsock;
		if ((n = select(maxsock + 1, &readable, (fd_set *)0,
		    (fd_set *)0, (struct timeval *)0)) <= 0) {
			if (n < 0 && errno != EINTR) {
				syslog(LOG_WARNING, "select: %m");
				sleep(1);
			}
			continue;
		}
		/* handle any queued signal flags */
		if (FD_ISSET(signalpipe[0], &readable)) {
			int nsig;
			if (ioctl(signalpipe[0], FIONREAD, &nsig) != 0) {
				syslog(LOG_ERR, "ioctl: %m");
				exit(EX_OSERR);
			}
			while (--nsig >= 0) {
				char c;
				if (read(signalpipe[0], &c, 1) != 1) {
					syslog(LOG_ERR, "read: %m");
					exit(EX_OSERR);
				}
				if (debug)
					warnx("handling signal flag %c", c);
				switch (c) {
				case 'C': /* sigchld */
					reapchild();
					break;
				case 'H': /* sighup */
					config();
					break;
				}
			}
		}
		for (sep = servtab; n && sep; sep = sep->se_next) {
			char t;

			if (sep->se_fd[0] == -1 ||
			    !FD_ISSET(sep->se_fd[0], &readable))
				continue;
			n--;
			if (debug)
				warnx("someone wants %s", sep->se_service);
			l = read(sep->se_fd[0], (void *)&t, sizeof(t));
			assert(l == 1);
			switch (t) {
			case 'L':
				launch(sep);
				break;
			default:
				warnx("Unknown command");
				break;
			}
		}
	}

	return (1);
}
